{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# *OPTIONAL* Separating the logic using classes\n",
    "\n",
    "As in the previous notebook, the goal here is to separate the logic (generating a string of characters given its length) from the user interface. \n",
    "\n",
    "This time, we creaate a class to hold the user interface, a class to represent the logic and a third class to connect them. \n",
    "\n",
    "Along the way, we'll see:\n",
    "\n",
    "+ a new way to link user interface elements to data, by giving the data traits that can be linked to widget traits\n",
    "+ how to validate the values of those traits\n",
    "+ how to properly subclass a an `ipywidgets` widget\n",
    "+ one way to link those two classes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import ipywidgets as widgets\n",
    "import traitlets"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## User interface, as a class"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This time we will build the password widget as a subclass of `VBox`. This has the virtue of making it easy to distribute and use just like one of `ipywidget`'s built in widgets.\n",
    "\n",
    "We'll go through a few iterations of this, with the final one called `PassGenGUI`; until we reach that point we'll add version numbers to the names."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this first version we construct the same interface as in the previous two notebooks.\n",
    "\n",
    "The easiest to overlook line in the code below is the one calling the superclass's `__init__`. If you forget that then your widget will not work even though you have subclassed from `VBox`. \n",
    "\n",
    "Notice that the individual widgets are \"private\" in the sense that their names are prepended with an underscore. If there are elements that you want to expose to the end user you can, of course, do that."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class PassGenGUI_incomplete(widgets.VBox):\n",
    "    def __init__(self):\n",
    "        # Do NOT forgot to do this ↓↓↓↓↓↓↓↓ \n",
    "        super(PassGenGUI_incomplete, self).__init__()\n",
    "        \n",
    "        # Define each of the children...\n",
    "        self._helpful_title = widgets.HTML('Generated password is:')\n",
    "        self._password_text = widgets.HTML('No password yet', placeholder='No password generated yet')\n",
    "        self._password_text.layout.margin = '0 0 0 20px'\n",
    "        self._password_length = widgets.IntSlider(description='Length of password',\n",
    "                                                  min=8, max=20,\n",
    "                                                  style={'description_width': 'initial'})\n",
    "        children = [self._helpful_title, self._password_text, self._password_length]\n",
    "        self.children = children"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's take a look at the widget..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pwd_gen = PassGenGUI_incomplete()\n",
    "pwd_gen"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that our widget doesn't have a value; it really seems like it should, and that the value should be the password itself. Though we could add that as a `traitlet` linked to the value of the `self._password_text`, we'll add a read-only property to the class instead. The reason is that we don't want users to be able to set the value of our widget's password. Its entire purpose is to generate a passsword for the user."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class PassGenGUI(widgets.VBox):\n",
    "    def __init__(self):\n",
    "        # Do NOT forgot to do this!\n",
    "        super(PassGenGUI, self).__init__()\n",
    "        \n",
    "        # Define each of the children...\n",
    "        self._helpful_title = widgets.HTML('Generated password is:')\n",
    "        self._password_text = widgets.HTML('No password yet', placeholder='No password generated yet')\n",
    "        self._password_text.layout.margin = '0 0 0 20px'\n",
    "        self._password_length = widgets.IntSlider(description='Length of password',\n",
    "                                                  min=8, max=20,\n",
    "                                                  style={'description_width': 'initial'})\n",
    "        children = [self._helpful_title, self._password_text, self._password_length]\n",
    "        self.children = children\n",
    "\n",
    "    # Add value as a read-only property\n",
    "    @property\n",
    "    def value(self):\n",
    "        return self._password_text.value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's try it out..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pwd_gen2 = PassGenGUI()\n",
    "pwd_gen2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Make sure you can get the value..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pwd_gen2.value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "...and that you cannot *set* the value"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pwd_gen2.value = 'new password'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## A logic class with linkable traits\n",
    "\n",
    "In the previous iteration of our password generator we wrote a function to generate a password, and a second function to add a callback, i.e. a call to that function whenever a control changes. There is nothing wrong with that approach, but it gets more complicated the more controls you add.\n",
    "\n",
    "Instead, we define a class below that generates the password, and includes as class attributes `traitlets` that we will later link to the widget GUI. These `traitlets` are like widget keys; they can be link to widget keys, the logic class can watch for changes in the keys and the logic class can set values of the keys.\n",
    "\n",
    "Note that nothing in this class refers to the widget; it's logic is entirely internal.\n",
    "\n",
    "To include traitlets a class must subclass from `traitlets.HasTraits`.\n",
    "\n",
    "We'll implment this in steps. First, in the cell below, we create the `PassGen` class to represent the password generating logic, ad a single trait called `length`, and define a method that is called when the value of length is changed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Subclass from HasTraits\n",
    "class PassGenLogic_v1(traitlets.HasTraits):\n",
    "    # Define your traits here, as class attributes \n",
    "    length = traitlets.Integer()\n",
    "    \n",
    "    def __init__(self):\n",
    "        # initialize the class by calling the superclass\n",
    "        super(PassGenLogic_v1, self).__init__()\n",
    "        \n",
    "    # The observe decorator is used to indicate that the function being\n",
    "    # decorated should be called if any of the traits listed as arguments\n",
    "    # change. The decorated function MUST have an argument named change;\n",
    "    # in this example the change variable isn't used because we want to update\n",
    "    # the generated password no matter what the change was.\n",
    "    \n",
    "    # Note that we are monitoring changes in PassGen's **own trait**, length,\n",
    "    # rather than a change in one of the controls in the GUI.\n",
    "    \n",
    "    @traitlets.observe('length')\n",
    "    def calculate_password(self, change):\n",
    "        import string\n",
    "        import secrets\n",
    "    \n",
    "        new_password = ''.join(secrets.choice(string.ascii_letters) for _ in range(self.length))\n",
    "        \n",
    "        # We don't return anything (explanation later). For now just print the \n",
    "        # password to observe what happens when we change the length.\n",
    "        print('In password generator password is: ', new_password)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We'll make an instance of the class so we can see what happens when we change the `length` attribute. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = PassGenLogic_v1()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Try changing the length property of in the cell below. Note that any time it is changed, a new password is generated."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.length = 20"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Add a trait to hold the password value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We don't actually want to print the password out each time we generate a new one, of course, and we eventually want to connect this to a widget-based GUI. The class below adds a new trait for the generated password, and sets that trait in `calculate_password`. You could, in addition, return the generated password; that could be useful in testing your model code, for example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Subclass from HasTraits\n",
    "class PassGenLogic_v2(traitlets.HasTraits):\n",
    "    # Define your traits here, as class attributes \n",
    "    length = traitlets.Integer()\n",
    "    \n",
    "    # The new trait is defined here:\n",
    "    password = traitlets.Unicode()\n",
    "    \n",
    "    def __init__(self):\n",
    "        super(PassGenLogic_v2, self).__init__()\n",
    "            \n",
    "    @traitlets.observe('length')\n",
    "    def calculate_password(self, change):\n",
    "        import string\n",
    "        import secrets\n",
    "    \n",
    "        new_password = ''.join(secrets.choice(string.ascii_letters) for _ in range(self.length))\n",
    "        \n",
    "        # Set the value of the password trait here:\n",
    "        self.password = new_password"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's try it out:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p2 = PassGenLogic_v2()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p2.length = 110\n",
    "p2.password"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that as currently written nothing prevents setting a nonsensical length:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p2.length = -14\n",
    "p2.password"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Add validation of the password length"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Traitlets provides a mechanism for validating values using the `@validate` decorator. The documentation on validation is [here](https://traitlets.readthedocs.io/en/stable/using_traitlets.html#validation), but the essential ideas are illustrated below. \n",
    "\n",
    "This is the final version of `PassGen`, so we drop the version at the end. \n",
    "\n",
    "Only the new lines are commented."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class PassGenLogic(traitlets.HasTraits):\n",
    "    length = traitlets.Integer()\n",
    "    password = traitlets.Unicode()\n",
    "    \n",
    "    def __init__(self):\n",
    "        super(PassGenLogic, self).__init__()\n",
    "            \n",
    "    @traitlets.observe('length')\n",
    "    def calculate_password(self, change):\n",
    "        import string\n",
    "        import secrets\n",
    "    \n",
    "        new_password = ''.join(secrets.choice(string.ascii_letters) for _ in range(self.length))\n",
    "        \n",
    "        # Set the value of the password trait here:\n",
    "        self.password = new_password\n",
    "\n",
    "    # The new validator:\n",
    "    @traitlets.validate('length')\n",
    "    # You can name this method anything you want.\n",
    "    def _validate_length(self, proposal):\n",
    "        # proposal contains the new value\n",
    "        length = proposal['value']\n",
    "        \n",
    "        # Test the value\n",
    "        if length < 1:\n",
    "            # if it is a bad value, raise a TraitError\n",
    "            raise traitlets.TraitError('Password length should be positive.')\n",
    "        # If the value is good, return it.\n",
    "        return proposal['value']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p3 = PassGenLogic()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Try setting a negative length in the cell below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p3.length = -23"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Putting the piece together"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that we have an interface, `PassGenGUI` and a class `PassGenLogic` to encapsulate the calcualtion we want to do, we need to connect them. \n",
    "\n",
    "We will do it by creating a third class, `PassGen`, that links the two. This is not the only approach -- the code below could be included as part of `PassGenGUI`, for example. \n",
    "\n",
    "It would be fine to complete separate the control code into a separate class. Whichever class contains the control code is the class the user should import.\n",
    "\n",
    "This is the final version of the `PassGenUI` so we drop the version from the end."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# By subclassing from PassGenGUI we ensure PassGen behaves as a widget, since\n",
    "# PassGenGUI subclasses from ipywidgets.VBox\n",
    "\n",
    "class PassGen(PassGenGUI):\n",
    "    def __init__(self):\n",
    "        # Set up all of the subwidgets like _passwordd)text\n",
    "        super(PassGen, self).__init__()\n",
    "        \n",
    "        # Create an instance of our model.\n",
    "        self.model = PassGenLogic()\n",
    "        \n",
    "        # Link the password from the model to the _password_text widget\n",
    "        traitlets.link((self.model, 'password'), (self._password_text, 'value'))\n",
    "        \n",
    "        # Link the _password_length widget to the length in the model\n",
    "        traitlets.link((self.model, 'length'), (self._password_length, 'value'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's create an instance of the class, display it, and try it out!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "password_widget = PassGen()\n",
    "password_widget"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "widgets-tutorial",
   "language": "python",
   "name": "widgets-tutorial"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
